#!/usr/bin/env python
# -*- coding: utf-8 -*-

import urllib2
import json
from base64 import b64encode
from getpass import getpass
from urllib import urlencode
from cookielib import CookieJar
from os.path import expanduser, exists
from pprint import pprint
from collections import OrderedDict
from argparse import ArgumentParser

USERNAME = ""
PASSWORD = ""
APIKEY_AUTH = "acfe292a432ded08e576f019fb4e34d104df16436"
APIKEY_ANON = "641ff5860e0a7e26e97e48d8ee80d162"
DEBUG = False


class ImgurError(Exception):
    pass


class Imgur(object):
    """
    This class can be used to upload images and manage an account on imgur.com
    """

    base = "api.imgur.com"
    connected = False
    urls = {
            # Anon
            "upload": "/2/upload",
            "image": "/2/image/%s",
            "delete": "/2/delete/%s",
            # Auth
            "signin": "/2/signin",
            "account": "/2/account",
            "account_images": "/2/account/images",
            "account_image": "/2/account/images/%s",
            "account_images_count": "/2/account/images_count",
            "albums": "/2/account/albums",
            "album": "/2/account/albums/%s",
            "albums_count": "/2/account/albums_count",
            "albums_order": "/2/account/albums_order",
            "album_order": "/2/account/albums_order/%s",
            # Other
            "stats": "/2/stats",
            "credits": "/2/credits",
           }
    
    def __init__(self, apikey_auth=None, apikey_anon=None, print_results=True):
        self.apikey_auth = apikey_auth
        self.apikey_anon = apikey_anon
        self.print_results = print_results
        self.cj = CookieJar()

    def _handle_response(fun):
        def new_f(self, *args, **kwargs):
            try:
                resp = fun(self, *args, **kwargs)
            except urllib2.HTTPError, err:
                raise ImgurError(err)
            else:
                if self.print_results:
                    content = resp.read()
                    try:
                        #pprint(json.loads(content))
                        json_print(json.loads(content))
                    except:
                        pprint(content)
                return resp
        return new_f

    def connection_required(fun):
        """ A decorator that checks if the user is authenticated """
        def new_f(self, *args, **kwargs):
            if not self.connected:
                raise ImgurError("You must be connected to perform this"
                        "operation")
            return fun(self, *args, **kwargs)
        return new_f

    def connect(self, username=None, password=None):
        """
        A cookie is created for the authentication
        """
        response = self.request("POST", "signin",
                username=username, password=password)
        debug("Connected.")
        self.connected = True

    def _json(self, data):
        try:
            return json.loads(data)
        except ValueError:
            raise ImgurError("Invalid data")

    @_handle_response
    @connection_required
    def _account(self):
        return self.request("GET", "account")

    @_handle_response
    def _upload(self, filename, url, name, title, caption):
        if filename:
            with open(expanduser(filename), "rb") as f:
                img = b64encode(f.read())
            t = "base64"
        else:
            img = url
            t = "url"
        url = "account_images" if self.connected else "upload"
        return self.request("POST", url, image=img,
                type=t, name=name, title=title, caption=caption)

    @_handle_response
    @connection_required
    def _imgedit(self, hash, title="", caption=""):
        url = self.urls["account_image"] % hash
        return self.request("POST", url=url, title=title, caption=caption)

    @_handle_response
    def _imginfos(self, hash):
        c = self.connected
        url = self.urls["account_image" if c else "image"] % hash
        return self.request("GET", url=url)

    @_handle_response
    def _imgdelete(self, hash):
        c = self.connected
        url = self.urls["account_image" if c else "delete"] % hash
        return self.request("DELETE", url=url)

    @_handle_response
    @connection_required
    def _imgcount(self):
        return self.request("GET", "account_images_count")

    @_handle_response
    @connection_required
    def _list(self):
        return self.request("GET", "account_images")

    @_handle_response
    @connection_required
    def _albums(self, title="", description="", privacy="", layout="",
            count="", page="", create=True):
        if create:
            return self.request("POST", "albums", title=title,
                    description=description, privacy=privacy, layout=layout)
        else:
            return self.request("GET", "albums", count=count, page=page)

    @_handle_response
    @connection_required
    def _albmlist(self, album_id):
        url = self.urls["album"] % album_id
        return self.request("GET", url=url)

    @_handle_response
    @connection_required
    def _albmdelete(self, album_id):
        url = self.urls["album"] % album_id
        return self.request("DELETE", url=url)

    @_handle_response
    @connection_required
    def _albmedit(self, album_id, title="", description="", cover="",
            privacy="", layout="", images="", add_images="", del_images=""):
        url = self.urls["album"] % album_id
        datas = {}
        for k in ("url", "title", "description", "cover", "privacy", "layout",
                "images", "add_images", "del_images"):
            datas[k] = locals()[k]
        return self.request("POST", **datas)

    @_handle_response
    @connection_required
    def _albmcount(self):
        return self.request("GET", "albums_count")

    @_handle_response
    @connection_required
    def _albmord(self, album_id, hashes):
        url = self.urls["album_order"] % album_id
        return self.request("POST", url=url, hashes=hashes)

    @_handle_response
    @connection_required
    def _albmords(self, album_ids):
        return self.request("POST", "albums_order", album_ids=album_ids)

    @_handle_response
    def _limits(self):
        return self.request("GET", "credits")

    def _stats(self, view):
        return self.request("GET", "stats", view=view)

    def request(self, method, key="", url="", headers=None, **params):
        # Send an http request.
        # if key is set, use the url from self.urls[key]
        # else use 'url'
        if not headers:
            headers = {}
        url = "http://" + self.base + (self.urls[key] if key else url) + ".json"
        params["key"] = self.apikey_auth if self.connected else self.apikey_anon
        data = None
        if method in ("POST", "DELETE"):
            for k in params.keys():
                if not params[k]:
                    del params[k]
            data = urlencode(params)
        elif method in ("GET", "DELETE"):
            url = "%s?%s" % (url, urlencode(params))

        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
        urllib2.install_opener(opener)
        debug(url, params)
        req = urllib2.Request(url, data, headers)
        if not method in ("POST", "GET"):
            req.get_method = lambda: method
        return urllib2.urlopen(req)
    
    def _get_json(self, fun_name, *args, **kwargs):
        return self._json(getattr(self, fun_name)(*args, **kwargs).read())

    def _infos(self, img):
        """ Print the infos of an image """
        return dict_print(OrderedDict(img["image"].items() +
                img["links"].items()))

    def _liststr(self, datas):
        """ Converts a list into "(foo,bar,baz)" """
        if isinstance(datas, (list, tuple)):
            datas = "(%s)" % (",".join(str(e) for e in datas))
        elif not datas.startswith("("):
            datas = "(%s)" % datas
        return datas

    def upload_image(self, image="", name="", title="", caption="", hashes=False):
        """
        Upload an image
        'image' can be a path or an url
        if hashes is True, return only the hashes
        """
        url, file = "", ""
        if not exists(image):
            url = image
        else:
            file = image
        datas = self._get_json("_upload", file, url, name, title, caption)
        infos = datas["images" if self.connected else "upload"]
        if hashes:
            return "Hash: %s\nDelete: %s" % (infos["image"]["hash"],
                    infos["image"]["deletehash"])
        else:
            return self._infos(infos)

    def account(self):
        """ Get account infos """
        datas = self._get_json("_account")
        return dict_print(datas["account"])

    def list_images(self):
        """ List all images in account """
        datas = self._get_json("_list")
        imgs = (self._infos(img) for img in datas["images"])
        return "\n *** \n".join(imgs)

    def infos_image(self, hash):
        """ Retrieve informations from an image """
        datas = self._get_json("_imginfos", hash)
        return self._infos(datas["images" if self.connected else "image"])

    def delete_image(self, hash):
        """ Delete an image """
        datas = self._get_json("_imgdelete", hash)
        return datas["images"]["message"] if self.connected else ""

    def edit_image(self, hash, new_title="", new_caption=""):
        """ Edit an image """
        datas = self._get_json("_imgedit", hash, new_title, new_caption)
        return self._infos(datas["images"])

    def count_images(self):
        """ Count all images in the account """
        return self._get_json("_imgcount")["images_count"]["count"]

    def list_albums(self, count=30, page=1):
        """
        List all albums in the account
        """
        datas = self._get_json("_albums", count=count, page=page,
                create=False)
        return "\n *** \n".join(dict_print(alb) for alb in datas["albums"])

    def create_album(self, title="", description="", privacy="", layout=""):
        """
        All values are optional.
        privacy: public/hidden/secret
        layout: blog/horizontal/vertical/grid
        """
        datas = self._get_json("_albums", title, description, privacy, layout)
        return dict_print(datas["albums"])

    def list_album(self, album_id):
        """ List all images in an album """
        datas = self._get_json("_albmlist", album_id)
        imgs = (self._infos(img) for img in datas["albums"])
        return "\n *** \n".join(imgs)

    def delete_album(self, album_id):
        """ Delete an album """
        datas = self._get_json("_albmdelete", album_id)
        return datas["albums"]["message"]

    def edit_album(self, album_id, title="", description="", cover="",
            privacy="", layout="", images="", add_images="", del_images=""):
        """
        Edit an album
        privacy: public/hidden/secret
        layout: blog/horizontal/vertical/grid
        cover: a hash of an image in the account
        'images' replaces all images in the album

        images, add_images and del_images must be a list of hashes,
            or a string formatted like (hash1,hash2,...)
        """
        for k in ("images", "add_images", "del_images"):
            locals()[k] = self._liststr(locals()[k])
        datas = self._get_json("_albmedit", album_id, title, description,
                cover, privacy, layout, images, add_images, del_images)
        imgs = (self._infos(img) for img in datas["albums"])
        return "\n *** \n".join(imgs)

    def count_albums(self):
        """ Return the number of albums. """
        datas = self._get_json("_albmcount")
        return datas["albums_count"]["count"]

    def order_album(self, album_id, hashes):
        hashes = self._liststr(hashes)
        datas = self._get_json("_albmord", album_id, hashes)
        return "\n *** \n".join(dict_print(alb) for alb in datas["albums_order"])

    def order_albums(self, album_ids):
        album_ids = self._liststr(album_ids)
        datas = self._get_json("_albmords", album_ids)
        imgs = (self._infos(img) for img in datas["albums_order"])
        return "\n *** \n".join(imgs)

    def stats(self, view="month"):
        """
        View imgur's stats
        view: today|week|month
        """
        return dict_print(self._get_json("_stats", view)["stats"])


def debug(*datas):
    if DEBUG:
        print "DEBUG:", ", ".join(str(d) for d in datas)

def json_print(data, tab=0):
    if isinstance(data, (list, tuple)):
        print
        for elem in data:
            json_print(elem, tab)
            print
    elif isinstance(data, dict):
        print
        for e, d in data.items():
            print "\t"*tab, e,
            json_print(d, tab+1)
    else:
        print "\t"*tab, data 

def dict_print(data):
    def align(l, r, w=80):
        return "%s: %s%s" % (l, (w-len(l))* " ", r)
    w = max(len(e) for e in data.keys())
    return "\n".join(align(k, v, w) for k, v in data.items())

def main():
    i = Imgur(APIKEY_AUTH, APIKEY_ANON, False)

    # Parser definition
    parser = ArgumentParser(description="A command-line utility for imgur.com")
    parser.add_argument("--user")
    parser.add_argument("--password")
    parser.add_argument("--ask-pass", action="store_true",
            help="Prompt for a password, so that it will never be displayed.")
    parser.add_argument("--anon", action="store_true", default=False,
            help="Do not authenticate.")
    sparsers = parser.add_subparsers(metavar="ACTION", dest='sp_name', 
            help="Use '%(prog)s action -h' for more help")
    
    up_parser = sparsers.add_parser("upload", help="Upload a file or an url")
    up_parser.add_argument("image", help="A path or an url")
    up_parser.add_argument("-t", "--title")
    up_parser.add_argument("-n", "--name")
    up_parser.add_argument("-c", "--caption")
    up_parser.add_argument("-s", "--hashes", action="store_true",
            help="Only print the hashes instead of all the informations")

    imgd_parser = sparsers.add_parser("delete", help="Delete an image")
    imgd_parser.add_argument("hash")

    imgi_parser = sparsers.add_parser("infos", help="Get infos on the image")
    imgi_parser.add_argument("hash")

    imge_parser = sparsers.add_parser("edit", help="Edit an image")
    imge_parser.add_argument("hash")
    imge_parser.add_argument("-t", "--title")
    imge_parser.add_argument("-c", "--caption")

    list_parser = sparsers.add_parser("list", help="List all your images")

    acc_parser = sparsers.add_parser("account", help="Account infos")

    albc_parser = sparsers.add_parser("create-album", help="Create a new album")
    albc_parser.add_argument("-t", "--title")
    albc_parser.add_argument("-d", "--description")
    albc_parser.add_argument("-p", "--privacy", choices=["public", "hidden",
        "secret"])
    albc_parser.add_argument("-l", "--layout", choices=["blog", "horizontal",
        "vertical", "grid"])

    albe_parser = sparsers.add_parser("edit-album", help="Edit an album")
    albe_parser.add_argument("album_id")
    albe_parser.add_argument("-t", "--title")
    albe_parser.add_argument("-d", "--description")
    albe_parser.add_argument("-p", "--privacy", choices=["public", "hidden",
        "secret"])
    albe_parser.add_argument("-l", "--layout", choices=["blog", "horizontal",
        "vertical", "grid"])
    albe_parser.add_argument("-c", "--cover", help="The hash of an image to use "
            "as cover for the album", metavar="hash")
    albe_parser.add_argument("-i", "--images", metavar="hash1,hash2,..,hashN",
            help="Replace all images in an album.")
    albe_parser.add_argument("-a", "--add", metavar="hash1,hash2,..,hashN",
            help="Add images to the album.")
    albe_parser.add_argument("-r", "--delete", metavar="hash1,hash2,..,hashN",
            help="Delete images from the album.")

    albl_parser = sparsers.add_parser("list-albums",
            help="List all albums.")
    albl_parser.add_argument("-c", "--count", help="The number of albums to return")
    albl_parser.add_argument("-p", "--page", help="The pages of albums to return")

    albs_parser = sparsers.add_parser("list-album",
            help="List all images in an album")
    albs_parser.add_argument("album_id")

    albco_parser = sparsers.add_parser("count-albums",
            help="Get the number of albums")

    albd_parser = sparsers.add_parser("delete-album", help="Delete an album")
    albd_parser.add_argument("album_id")

    albos_parser = sparsers.add_parser("order-albums", help="Order all albums in the account")
    albos_parser.add_argument("album_ids", metavar="id1,id2,...,idn", help="The new list of album IDs")

    albo_parser = sparsers.add_parser("order-album", help="Order all images in an album")
    albo_parser.add_argument("album_id")
    albo_parser.add_argument("hashes", metavar="hash1,hash2,...,hashn", help="The new list of hashes")

    stats_parser = sparsers.add_parser("stats", help="Imgur statistics")
    stats_parser.add_argument("-v", "--view",
            choices=["today", "month", "week"])

    # Authentification
    args = parser.parse_args()
    u, p, ap = args.user, args.password, args.ask_pass
    if u and not p:
        if ap:
            i.connect(u, getpass())
        else:
            i.connect(u, PASSWORD)
    elif p and not u:
        i.connect(USERNAME, p)
    elif u and p:
        i.connect(u, p)
    elif USERNAME and (not PASSWORD or ap) and not args.anon:
        i.connect(USERNAME, getpass())
    elif USERNAME and PASSWORD and not args.anon:
        i.connect(USERNAME, PASSWORD)
    
    # Preparation
    opts = vars(args)
    spname = args.sp_name
    del opts["user"], opts["password"], opts["anon"], opts["ask_pass"]
    if spname in ("upload", "infos", "delete", "edit", "list", "count"):
        fun_name = spname + "_image" if hasattr(Imgur,
                spname + "_image") else spname + "_images"
    elif spname.rstrip("s").endswith("album"):
        fun_name = spname.replace("-", "_")
    else:
        fun_name = spname
    del opts["sp_name"]
    fun = getattr(Imgur, fun_name)
    
    # And action.
    print fun(i, **opts)

if __name__ == "__main__":
    main()

